package bot.talk;

import game.model.Card;
import glassfrog.model.Gamedef;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

import util.DebugOut;

import bot.Bot;

/**
 * This class talks to the poker server. All message protocol dependent stuff should be in here.
 * @author Witthold/Korol
 */
public class Talk_ACPC extends Talk {

	/** Terminates the messages. The current message terminator is CR LF (13 10). */
	public static final String messageTerminator = "" + ((char)13)+((char)10);
//	public static final String messageTerminator = "\n"; // TODO change back into "\r\n" after ACPC server update
	
	private final String ClassOfPlayer = "GUIPLAYER";	/* TODO change to AAAIPLAYER when participating in ACPC tournament */
	//private final String ClassOfPlayer = "AAAIPLAYER";				/* (the server starts then the players via scripts)*/

	/**
	 * In a tournament the bot will be started via script and therefore announce itself as "AAAIPLAYER".</br>
	 * For testing it is easier to start the bot without script and it will announce itself as "GUIPLAYER".
	 */
	private final boolean tournament = false;
	
	/** Socket connecting to the server. */
	private Socket socket;
	/** Stream from the server. */
	private InputStream is;
	/** Stream to the server. */
	private OutputStream os;
	/** Has an ENDGAME signal been received? */
	private boolean matchOver;
	/** Is the round over? */
	private boolean gameOver = false;

	private boolean protocolNoLimit = false;

	/**
	 * This is the current game state.</br>
	 * It is not changed during a call to handleStateChange().</br>
	 * Necessary for sending a reply (by appending the action to the game state string).
	 */
	private String lastGameStateString;


	/**
	 * Calls super constructor with the given parameters.
	 * @param bot
	 * @param serverAdress
	 * @param port
	 */
	public Talk_ACPC(Bot bot, String serverAdress, int port) {

		super(bot, serverAdress, port);
	}


	/**
	 * Tries to connect to the given server (and enters a room).</br>
	 * Gets and sets the gamedef in the process.
	 */
	@Override
	public boolean initConnection() {

		InetAddress iaddr;

		try {

			iaddr = InetAddress.getByName(serverAdress);
			connect(iaddr, port);

			String name = bot.getName();
			int buyin = bot.getBuyin();

			if(tournament) {
				
				sendMessage("AAAIPLAYER:" + name + ":" + buyin);
				
			} else {
				
				sendMessage("GUIPLAYER:" + name + ":" + buyin);
			}
			

			ObjectInputStream ois = new ObjectInputStream(is);

			boolean playable = false;

			Gamedef gamedef;
			try {
				gamedef = (Gamedef) ois.readObject();

				playable = bot.newGame(gamedef);

				if (gamedef.isNoLimit()) {

					protocolNoLimit = true;
				}

			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			}

			if (playable) {

				BufferedReader br = new BufferedReader(new InputStreamReader(is));
				String portString = br.readLine();
				String[] st = portString.split(":");
				int roomPort = Integer.valueOf(st[1]).intValue();

				connect(iaddr, roomPort);

				matchOver = false;
				sendMessage("VERSION:1.0.0");

			} else {

				// TODO is there an abort message to send to the server?
			}

		} catch (UnknownHostException e) {

			e.printStackTrace();
			return false;

		} catch (IOException e) {

			e.printStackTrace();
			return false;
		}
		DebugOut.showVerboseMain("Successful connection to " + iaddr);
	
		return true;
	}


	public void run() {
		try {
			gameLoop();
			close();
		} catch (Exception e) {
			e.printStackTrace();
			System.err.println(e);
			System.exit(0);
		}
	}


	@Override
	public boolean sendFold() {

		return sendAction('f');
	}


	@Override
	public boolean sendCall() {

		return sendAction('c');
	}


	@Override
	public boolean sendRaise() {
		return sendAction('r');
	}


	@Override
	public boolean sendRaise(int finalInPot) {

		if (protocolNoLimit) {

			return sendAction("r" + finalInPot);

		} else {

			return sendAction('r');
		}
	}


	private void gameLoop() throws IOException {

		while (!gameOver) {

			String message = receiveMessage();

			if (message.startsWith("MATCHSTATE:")) { /* normal game state update */

				processMessage(message);
				lastGameStateString = message;
				bot.handleStateChange();

			} else if (message.startsWith("#PLAYERS")) { /* player information */

				String temp = message.substring(10);

				String[] playerDesc = temp.split("\\|\\|");

				DebugOut.showVerboseBotTalk("Setting " + (playerDesc.length) + " players ...");

				setPlayers(playerDesc);

			} else if (message.startsWith("#SHOWDOWN")) {	/* end of hand information */

				setEnemyHoleCards(lastGameStateString);

				String temp = message.substring(11);

				String[] winnerStrings = temp.split(":");

				String[] winner = new String[winnerStrings.length];
				int[] amount = new int[winnerStrings.length];

				for (int i = 0; i < winnerStrings.length; i++) {

					String[] perWinner = winnerStrings[i].split(" ");
					winner[i] = perWinner[0];
					amount[i] = Integer.valueOf(perWinner[2]);
				}

				bot.showdown(winner, amount);

				DebugOut.showVerboseBotTalk(message);
				bot.printState();

			} else if (message.equals("#GAMEOVER")) {	/* end of competition */

				bot.gameOver();

				gameOver = true;

				DebugOut.showVerboseBotTalk(message);
				DebugOut.showVerboseMain("Closing socket,...");
				is.close();
				os.close();
				socket.close();

			} else if (message.equals("ENDGAME")) {		/* in current server version obsolete */

				DebugOut.showVerboseBotTalk("ENDGAME");
				break;
			}
		}
	}


	/**
	 * Tries to set up a socket and to get the input and output stream. 
	 * @param iaddr
	 * @param port
	 * @throws IOException
	 */
	private void connect(InetAddress iaddr, int port) throws IOException {
		socket = new Socket(iaddr, port);
		is = socket.getInputStream();
		os = socket.getOutputStream();
	}


	/**
	 * Sends the given message to the server. (After appending the message terminator)
	 */
	private synchronized boolean sendMessage(String message) {
		DebugOut.showVerboseBotTalk("CLIENT SENDS:" + message);
		try {
			message = message + messageTerminator;
			byte[] messageData = message.getBytes();

			if (!matchOver) {
				os.write(messageData);
				return true;
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return false;
	}


	/**
	 * Closes the connection. Called after game loop ends.
	 */
	private synchronized void close() throws IOException {
		matchOver = true;
		try {
			Thread.sleep(1000);
		} catch (InterruptedException ie) {
		}
		os.close();
		is.close();
		socket.close();
	}


	/**
	 * Tests if the message is complete (contains a terminal character)
	 */
	private boolean isComplete(String message) {
		return message.endsWith(messageTerminator);
	}


	/**
	 * Send an action (action should be 'r', 'c', or 'f').</br>
	 * Usually called during handleStateChange.</br>
	 * Action will be in response to the state in currentGameStateString.
	 */
	private boolean sendAction(char action) {
		DebugOut.showVerboseBotTalk("action: " + action);
		return sendAction("" + action);
	}


	/**
	 * Send an action string (action should be r??, c, or f, 
	 * where ?? is the final amount in the pot from a player in chips).</br>
	 * Usually called during handleStateChange.</br>
	 * Action will be in response to the state in currentGameStateString.
	 */
	private boolean sendAction(String action) {
		DebugOut.showVerboseBotTalk(lastGameStateString + ":" + action);
		return sendMessage(lastGameStateString + ":" + action);
	}


	/**
	 * Receive a message from the server. (Removes the message terminator)
	 */
	private String receiveMessage() {

		StringBuffer responseBuffer = new StringBuffer();

		try {
			do {
				char c;
				c = (char) (is.read());
				responseBuffer.append(c);

			} while (!isComplete(responseBuffer.toString()));

			String response = responseBuffer.substring(0, responseBuffer.length() - messageTerminator.length());
			DebugOut.showVerboseBotTalk("CLIENT RECEIVES:" + responseBuffer);

			return response;

		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}
	}


	/**
	 * Processes the given message.</br>
	 * Extracts all the information in it and calls the according methods to intialize the hand, store the board cards, ...
	 * @param message	The message containing the data which are to set.
	 */
	private void processMessage(String message) {

		String[] splittedMessage = message.split(":");

		int ownPosition = Integer.valueOf(splittedMessage[1]);
		int hand = Integer.valueOf(splittedMessage[2]);
		String gameStorageString = splittedMessage[3];

		String[] cardStringArray = splittedMessage[4].split("/"); /* cardStringArray[0]: hole cards; cardStringArray[1-n]: public cards */
		String[] holeCardStrings = cardStringArray[0].split("\\|");

		Card[][] holeCards = new Card[holeCardStrings.length][];

		for (int i = 0; i < holeCards.length; i++) {

			holeCards[i] = extractCards(holeCardStrings[i]);
		}

		Card[] boardCards = null;
		if (cardStringArray.length > 1) {

			boardCards = extractCards(cardStringArray[1]);
		}

		if (gameStorageString.isEmpty() || gameStorageString.matches("^(b[0-9]+)+$")) { /* first message of the hand */

			initHand(ownPosition, hand, holeCards[ownPosition], boardCards);

			bot.setBlinds();

		} else if (!gameStorageString.endsWith("/")) { // "ordinary" gameStorage message or last round ended 	

			String[] splittedOldMessage = lastGameStateString.split(":");

			if (splittedMessage[3].equals(splittedOldMessage[3])) { /* last round ended */

				DebugOut.showVerboseBotTalk("Last round ended!");
				bot.endLastRound(holeCards);

			} else { /* "ordinary" message */

				DebugOut.showVerboseBotTalk("Ordinary message!");
				updateState(gameStorageString);
			}

		} else { /* ends with "/" (a round not equal the last ended)
			 		-> new cards are revealed (plus the last action has to be stored) */ 

			/* store last action */
			updateState(gameStorageString);

			/* store new board cards */

			if (cardStringArray.length > 1) {

				setBoardCards(cardStringArray.length - 1, cardStringArray[cardStringArray.length - 1]);
			}
		}
	}


	/**
	 * Extracts the cards from the given string and makes the bot set the given cards to the given round.
	 * 
	 * @param round Round in which the cards are revealed
	 * @param cardString Cards which are revealed
	 */
	private void setBoardCards(int round, String cardString) {

		Card[] cards = extractCards(cardString);
		bot.setBoardCards(round, cards);
	}


	/**
	 * Extracts the string representing the last action out of the given string.
	 * @param roundHistory	String representing the actions of the round
	 * @return	A string representing the last action of the given string
	 */
	private String extractLastAction(String roundHistory) {

		StringBuffer strB = new StringBuffer();

		for (int i = roundHistory.length() - 1; i >= 0; i--) {

			strB.insert(0, roundHistory.charAt(i));

			if (strB.charAt(0) == 'b' || strB.charAt(0) == 'f' || strB.charAt(0) == 'c' || strB.charAt(0) == 'r') {

				break;
			}
		}

		return strB.toString();
	}


	/**
	 * Updates the state of the bot depending of the given string.
	 * @param gameStorageString	String containing the changes to the state
	 */
	private void updateState(String gameStorageString) {

		/* get substring for current round */
		String[] gameStoragePerRound = gameStorageString.split("/");
		int currentRound = gameStoragePerRound.length - 1;

		String roundHistory = gameStoragePerRound[currentRound];

		for (String string : gameStoragePerRound) {
			DebugOut.showVerboseBotTalk("gameStoragePerRound: " + string);
		}

		/* get last action of current round */
		
		if (gameStorageString.length() > 0 && !gameStorageString.endsWith("/")) { /* it's not a new round */

			String actionString = extractLastAction(roundHistory);

			char action = actionString.charAt(0);

			if (actionString.length() == 1) {

				bot.addAction(currentRound, action);

			} else {

				int finalInPot = Integer.valueOf(actionString.substring(1));
				bot.addAction(currentRound, action, finalInPot);
			}

		} else { /* new round */

			if (roundHistory.isEmpty() || roundHistory.equals("")) { /* first round, nothing happened */ 

				// TODO is here anything to do?

			} else { /* not the first round, but a new one */

				String actionString = extractLastAction(roundHistory);

				char action = actionString.charAt(0);

				if (actionString.length() == 1) {

					bot.addAction(currentRound, action);

				} else {

					int finalInPot = Integer.valueOf(actionString.substring(1));
					bot.addAction(currentRound, action, finalInPot);
				}

				bot.newRound();
			}
		}
	}


	/**
	 * Initialize the hand.
	 * 
	 * @param position Own position
	 * @param hand Hand count
	 * @param holeCards own hole cards
	 * @param boardCards board cards which are revealed at the start of the hand // TODO are there any poker types with that?
	 */
	private void initHand(int position, int hand, Card[] holeCards, Card[] boardCards) {

		bot.setHand(position, hand);

		bot.setHoleCards(0, holeCards);

		bot.setBoardCards(0, boardCards);
	}


	/**
	 * Extracts the Cards from the given string and returns it.
	 * 
	 * @param cardString A string representing the card which are to be returned
	 * @return - A Card array containing all cards in the string</br>
	 *         - or null, if the string is empty
	 */
	private Card[] extractCards(String cardString) {

		if (cardString.isEmpty()) {
			return null;

		} else {

			Card[] cards = new Card[cardString.length() / 2];

			for (int i = 0; i < cardString.length(); i = i + 2) {

				cards[i / 2] = new Card(cardString.substring(i, i + 2));
			}

			return cards;
		}
	}


	/**
	 * Extracts all hole cards from the given string and calls the method of the bot to set them.
	 * 
	 * @param message A string containing the information about the revealed hole cards
	 */
	private void setEnemyHoleCards(String message) {

		String[] splittedMessage = message.split(":");

		String[] cardStringArray = splittedMessage[4].split("/"); /* cardStringArray[0]: hands; cardStringArray[1-n]: public cards */
		String[] holeCardStrings = cardStringArray[0].split("\\|");

		Card[][] holeCards = new Card[bot.getNumPlayers()][];

		for (int i = 0; i < holeCards.length && i < holeCardStrings.length; i++) {
			DebugOut.showVerboseBotTalk("holeCardStrings[" + i + "]: " + holeCardStrings[i]);
			if (holeCardStrings[i].length() > 0) {

				holeCards[i] = extractCards(holeCardStrings[i]);
			}
		}

		bot.endLastRound(holeCards);
	}


	/**
	 * Extracts the player information for each player (names and buy ins) and calls the according method of the bot.
	 * 
	 * @param playerDesc
	 */
	private void setPlayers(String[] playerDesc) {

		int n = playerDesc.length;
		String[] names = new String[n];
		int[] buyins = new int[n];

		for (int i = 0; i < n; i++) {

			String[] playerI = playerDesc[i].split(":");
			names[i] = playerI[1];
			buyins[i] = Integer.valueOf(playerI[2]);
		}

		bot.setPlayers(names, buyins);
	}
}